package com.nulldev.util.net.udp;

import java.io.Closeable;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;

import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.math.MathUtil;
import com.nulldev.util.net.dsap.Server;
import com.nulldev.util.net.packets.NetInfo;
import com.nulldev.util.net.packets.NetInfoAdapter;
import com.nulldev.util.net.packets.NetPacket;
import com.nulldev.util.net.packets.PacketManager;
import com.nulldev.util.other.CharsetUtil;
import com.nulldev.util.threading.fastES.LtqExecutor;
import com.nulldev.util.threading.v2.ThreadExecution;
import com.nulldev.util.threading.v2.ThreadManager;
import com.nulldev.util.threading.v2.ThreadPriority;
import com.nulldev.util.threading.v2.nThread;
import com.nulldev.util.threading.v3.IWorker;
import com.nulldev.util.threading.v3.ThreadDispatcher;

public abstract class UDPServer implements Runnable, Closeable, Server {

	public boolean useFastMode;
	private final Logger log = LoggerFactory.getLogger(true, true);
	private DatagramSocket socket;
	private int maxDataSize, port = new Integer(0);
	private BigDecimal tickRate = new BigDecimal(0);
	private InetAddress ip_c;

	/**
	 * Better Threading
	 * <br> - Increase in perfomance expected
	 * <br> - Probably can handle more players with less lag.
	 * <br> REPLACED BY FAST MODE in nullUtil 2.6
	 */
	@Deprecated
	private ExecutorService localService;

	/**
	 *
	 * @param port (Server Port goes here)
	 * @param tickRate (# of times listens/replies in a second, to calculate -> (1000 / (tickrate / 2)))
	 * (note: for most updates and stablity use a tickRate of 16 for 125 updates a second.)
	 * @param maxDataSize (Maximum data size in length) -> affects inbound
	 * @throws Exception (you know what this is)
	 */
	public UDPServer(int port, int tickRate, int maxDataSize) throws Exception{
		this.log.warn("Server is unsupported.");
//		this.localService = Executors.newCachedThreadPool();
		this.socket = new DatagramSocket(this.port = port);
		this.tickRate = new BigDecimal((double) 1000 / tickRate);
		this.maxDataSize = maxDataSize;
		this.ip_c = this.socket.getLocalAddress();
	}

	/**
	 * @param ip (Server IP goes here)
	 * @param port (Server Port goes here)
	 * @param tickRate (# of times listens/replies in a second, to calculate -> (1000 / (tickrate / 2)))
	 * (note: for most updates and stablity use a tickRate of 16 for 125 updates a second.)
	 * @param maxDataSize (Maximum data size in length) -> affects inbound
	 * @throws Exception (you know what this is)
	 */
	public UDPServer(final InetAddress ip, int port, int tickRate, int maxDataSize) throws Exception {		
		this.log.warn("Server is unsupported.");
//		this.localService = Executors.newWorkStealingPool();
		this.socket = new DatagramSocket(this.port = port, ip);
		this.tickRate = new BigDecimal((double) 1000 / tickRate);
		this.maxDataSize = maxDataSize;
		this.ip_c = ip;
	}

	public void start() {
		this.stopped = false;
		if (this.socket == null || this.socket.isClosed()) {
			try {
				this.socket = new DatagramSocket(this.port = port, this.ip_c);
			} catch (SocketException e) {
			}
		}
		if (this.localService == null) {
			this.localService = Executors.newCachedThreadPool();
		}
		this.localService.execute(this);
	}

	public int getMaxDataSize() {
		return this.maxDataSize;
	}

	public void setMaxDataSize(int size) {
		this.maxDataSize = MathUtil.clamp(2000, 60000, size);
	}
	
	@Deprecated
	/**
	 * Replaced with ThreadDispatcher (a.k.a Thread API v3) <br>
	 * It is faster than v2.
	 */
	private final ThreadManager tm = new ThreadManager();
	/**
	 * Experimental
	 */
	private final ThreadDispatcher td = new ThreadDispatcher(new LtqExecutor(128));
	
	private boolean stopped;

	/**
	 * Listener
	 * Listens from any incoming data, add own method to handle
	 */
	@Override
	public void run() {
		this.log.info("Started server. [IP: " + this.getIP() + ", port: " + this.port + "]");
		if (this.useFastMode) {
			while (true) {
				if (this.stopped) {
					break;
				}
				try{
					if (this.socket == null || this.socket.isClosed()) {
						return;
					}
					byte[] data = new byte[UDPServer.this.maxDataSize];
					DatagramPacket packet = new DatagramPacket(data, data.length);
					this.socket.setSoTimeout(tickRate.intValue());
					this.socket.receive(packet);
					if (packet.getLength() > 0) {
//						this.tm.addTask(new nThread(new Runnable() {
//							
//							@Override
//							public void run() {
//								try{
//									UDPServer.this.handle(packet);
//									Thread.sleep(UDPServer.this.tickRate.longValue());
//								}catch (Exception e){
//									if (e.getClass() == SocketTimeoutException.class){
//									}else{
//										e.printStackTrace();
//									}
//								}
//							}
//						}, "$UDPv2.fastMode", ThreadPriority.NORMAL));
						this.td.addTask(new IWorker() {
							
							@Override
							public void run() {
								try{
									UDPServer.this.handle(packet);
									Thread.sleep(UDPServer.this.tickRate.longValue());
								}catch (Exception e){
									if (e.getClass() == SocketTimeoutException.class){
									}else{
										e.printStackTrace();
									}
								}
							}
							
							@Override
							public void shutdown() {
							}
							
							@Override
							public boolean isAlive() {
								return true;
							}
							
							@Override
							public String getName() {
								return "$UDP.fastMode";
							}
							
							@Override
							public int getID() {
								return MathUtil.randInt(0, 100000000);
							}
						});
					}
				}catch (Exception e){
					if (e.getClass() == SocketTimeoutException.class || e.getClass() == SocketException.class || 
							e.getMessage().equals("Socket is closed") || e.getMessage().equals("Socket closed")) {
						
					}else{
						e.printStackTrace();
					}
				}
			}
		}else {
			while (true){
				if (this.stopped) {
					break;
				}
				if (this.socket.isClosed()) {
					break;
				}
				try{
					byte[] data = new byte[this.maxDataSize];
					DatagramPacket packet = new DatagramPacket(data, data.length);
					this.socket.setSoTimeout(tickRate.intValue());
					this.socket.receive(packet);
					this.handle(packet);
					Thread.sleep(this.tickRate.longValue());
				}catch (Exception e){
					if (e.getClass() == SocketTimeoutException.class || e.getClass() == SocketException.class || 
							e.getMessage().equals("Socket is closed") || e.getMessage().equals("Socket closed")){

					}else{
						e.printStackTrace();
					}
				}
			}
		}
	}

	/**
	 * Replies to client with byte[]
	 * @param data
	 * @param ip -> use packet.getAddress()
	 * @throws Exception
	 */
	public void reply(byte[] data, InetAddress ip, int port) throws Exception{
		if (data.length > maxDataSize) {
			throw new Exception("The packet is larger than the maxDataSize (" + (data.length - maxDataSize) + " too large)");
		}
		DatagramPacket packet = new DatagramPacket(data, data.length, ip, port);
		socket.send(packet);
	}

	public void reply(String text, InetAddress ip, int port) throws Exception{
		if (text.getBytes(UTF8_CHARSET).length > maxDataSize) {
			throw new Exception("The packet is larger than the maxDataSize (" + (text.getBytes(UTF8_CHARSET).length - maxDataSize) + " too large)");
		}
		final byte[] data = text.getBytes("UTF-16LE");
		DatagramPacket packet = new DatagramPacket(data, data.length, ip, port);
		socket.send(packet);
	}

	public void reply(String text, DatagramPacket p) throws Exception{
		if (text.getBytes(UTF8_CHARSET).length > maxDataSize) {
			throw new Exception("The packet is larger than the maxDataSize (" + (text.getBytes(UTF8_CHARSET).length - maxDataSize) + " too large)");
		}
		final byte[] data = text.getBytes("UTF-16LE");
		DatagramPacket packet = new DatagramPacket(data, data.length, p.getAddress(), p.getPort());
		socket.send(packet);
	}

	public void reply(final NetPacket packets, DatagramPacket p) throws Exception{
		final byte[] data = packets.toString().getBytes("UTF-16LE");
		DatagramPacket packet = new DatagramPacket(data, data.length, p.getAddress(), p.getPort());
		socket.send(packet);
	}

	public void reply(final NetPacket packets, final InetAddress ip, final int port) throws Exception{
		final byte[] data = packets.toString().getBytes("UTF-16LE");
		DatagramPacket packet = new DatagramPacket(data, data.length, ip, port);
		socket.send(packet);
	}

	/**
	 * Core Handler, use for basic connections
	 * @param p
	 * @throws Exception
	 */
	public void handle(DatagramPacket p) throws Exception{
		if (p.getData().length == 0){
			return;
		}
		final NetInfo na = NetInfoAdapter.create(this);
		final String data = convertToString(p.getData()).trim().replaceAll(Pattern.quote("%space"), " ");
		final NetPacket packet = PacketManager.parsePacket(this, na, data);
		if (!packet.isGeneric()) {
			packet.server(packet.getData(), data, na);
		}else {
			this.ext_handle(p, data);
		}
	}

	public abstract void ext_handle(DatagramPacket p, final String data);

	protected final Charset UTF8_CHARSET = CharsetUtil.UTF_16LE;

	public String convertToString(byte[] data){
		return new String(data, UTF8_CHARSET).trim();
	}

	public int getTickRate(){
		return (int) this.tickRate.intValue();
	}

	public int getMaxBytes(){
		return this.maxDataSize;
	}

	public int getBetterTickRate(){
		return (int) (1000 / this.tickRate.doubleValue());
	}

	public int getPort() {
		return this.port;
	}

	public String getIP() {
		if (this.socket == null || this.socket.getLocalAddress() == null) {
			return "";
		}
		final String i0 = this.socket.getLocalAddress().getHostAddress();
		if (i0 == null) {
			return "";
		}
		return i0;
	}

	public DatagramSocket i(){
		return this.socket;
	}

	@Override
	public void close() throws IOException {
		this.socket.close();
		this.stopped = true;
		if (this.useFastMode) {
			this.tm.kill();
		}
	}

	public String format() {
		return this.getIP() + ":" + this.getPort();
	}
	
	@Override
	public boolean supportsSDTP() {
		return true;
	}
	
	@Override
	public boolean isDSAPready() {
		return false;
	}
	
	@Override
	public boolean supportsDSAP_SecureStreams() {
		return false;
	}

	@Override
	public boolean isDSAPSpawn() {
		return false;
	}
	
	@Override
	public void closeDSAP() {
		if (this.isDSAPSpawn()) {
			try {
				this.close();
				LoggerFactory.getLoggerName("DSAP", true, true).info("Allocated server on " + this.getIP() + ":" + this.getPort() + " was closed.");
			} catch (IOException e) {
			}
		}
	}
}

